package com.train.order.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.train.api.TicketApi;
import com.train.common.constants.KeyConstants;
import com.train.common.enums.impl.BizExceptionEnums;
import com.train.common.enums.impl.SysExceptionEnums;
import com.train.common.exception.GlobalException;
import com.train.common.resp.CommonResp;
import com.train.common.resp.PageResp;
import com.train.common.utils.collection.CollectionUtils;
import com.train.common.utils.convert.JsonConvertUtils;
import com.train.common.utils.date.LocalDateTimeUtils;
import com.train.common.utils.spring.SpringUtils;
import com.train.daily.api.DailyTrainCarriageApi;
import com.train.daily.api.DailyTrainSeatApi;
import com.train.daily.api.DailyTrainSkTokenApi;
import com.train.daily.api.DailyTrainTicketApi;
import com.train.daily.dto.carriage.DailyTrainCarriageQueryListDTO;
import com.train.daily.dto.seat.DailyTrainSeatEditChoiceSeatDTO;
import com.train.daily.dto.seat.DailyTrainSeatQueryListDTO;
import com.train.daily.dto.ticket.DailyTrainTicketQueryOneDTO;
import com.train.daily.dto.ticket.DailyTrainTicketReduceTicketDTO;
import com.train.daily.dto.token.DailyTrainSkTokenValidDTO;
import com.train.daily.dto.token.DailyTrainTokenQueryLeftDTO;
import com.train.daily.resp.carriage.DailyTrainCarriageQueryListResp;
import com.train.daily.resp.seat.DailyTrainSeatQueryListResp;
import com.train.daily.resp.ticket.DailyTrainTicketQueryOneResp;
import com.train.daily.resp.token.DailyTrainTokenQueryLeftResp;
import com.train.dto.TicketDTO;
import com.train.openfeign.core.utils.OpenFeignUtils;
import com.train.order.bo.ConfirmOrderTicketBO;
import com.train.order.dto.confirm.*;
import com.train.order.enums.impl.ConfirmOrderStatusEnums;
import com.train.order.mapper.ConfirmOrderMapper;
import com.train.order.mq.producer.ConfirmOrderProducer;
import com.train.order.po.ConfirmOrderPO;
import com.train.order.resp.ConfirmOrderPageResp;
import com.train.order.service.ConfirmOrderService;
import com.train.redis.core.support.RedisSupport;
import com.train.redis.core.support.RedissonSupport;
import com.train.redis.core.utils.SerializableUtils;
import com.train.system.enums.impl.SeatColEnums;
import com.train.system.enums.impl.SeatTypeEnums;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * @author baitiaojun
 * @description 针对表【confirmOrder(确认订单)】的数据库操作Service实现
 * @createDate 2025-03-11 21:07:57
 */
@Slf4j
@Service
//@RequiredArgsConstructor
public class ConfirmOrderServiceImpl extends ServiceImpl<ConfirmOrderMapper, ConfirmOrderPO>
    implements ConfirmOrderService {

    private final ConfirmOrderMapper confirmOrderMapper;
    private final DailyTrainTicketApi dailyTrainTicketApi;
    private final DailyTrainCarriageApi dailyTrainCarriageApi;
    private final DailyTrainSeatApi dailyTrainSeatApi;
    private final TicketApi ticketApi;
    private final DailyTrainSkTokenApi dailyTrainSkTokenApi;
    private final RedissonSupport redissonSupport;
    private final RedisSupport redisSupport;
    private final ConfirmOrderProducer confirmOrderProducer;

    public ConfirmOrderServiceImpl(ConfirmOrderMapper confirmOrderMapper,
                                   @Qualifier("com.train.daily.api.DailyTrainTicketApi") DailyTrainTicketApi dailyTrainTicketApi,
                                   @Qualifier("com.train.daily.api.DailyTrainCarriageApi") DailyTrainCarriageApi dailyTrainCarriageApi,
                                   @Qualifier("com.train.daily.api.DailyTrainSeatApi") DailyTrainSeatApi dailyTrainSeatApi,
                                   @Qualifier("com.train.api.TicketApi") TicketApi ticketApi,
                                   @Qualifier("com.train.daily.api.DailyTrainSkTokenApi") DailyTrainSkTokenApi dailyTrainSkTokenApi,
                                   RedissonSupport redissonSupport,
                                   RedisSupport redisSupport,
                                   ConfirmOrderProducer confirmOrderProducer) {
        this.confirmOrderMapper = confirmOrderMapper;
        this.dailyTrainTicketApi = dailyTrainTicketApi;
        this.dailyTrainCarriageApi = dailyTrainCarriageApi;
        this.dailyTrainSeatApi = dailyTrainSeatApi;
        this.ticketApi = ticketApi;
        this.dailyTrainSkTokenApi = dailyTrainSkTokenApi;
        this.redissonSupport = redissonSupport;
        this.redisSupport = redisSupport;
        this.confirmOrderProducer = confirmOrderProducer;
    }

    @Override
    public PageResp<ConfirmOrderPageResp> pageList(ConfirmOrderPageDTO confirmOrderPageDTO) {
        return confirmOrderMapper.pageList(confirmOrderPageDTO);
    }

    /**
     * 确认订单流程前置校验操作
     * @param dto
     */
    @Override
//    @SentinelResource(value = "confirmProcess",
//            blockHandlerClass = SentinelBlockHandler.class,
//            blockHandler="confirmOrderBlock") //定义限流资源
    public String confirmBeforeProcess(ConfirmOrderTicketDTO dto) {
        log.info("开始执行确认订单校验流程");
        LocalDate date = dto.getDate();
        String trainCode = dto.getTrainCode();
        String profile = SpringUtils.getProperty("spring.profiles.active");
        //开发环境不执行如下验证
        if (!"dev".equals(profile)) {
            //验证码校验,防刷
            this.validateCaptcha(dto);

            //令牌大闸,防止刷票
            this.preventTicketScalping(date, trainCode, dto.getMemberId());

            //唯一性校验, 检查订单是否存在
            ConfirmOrderQueryDTO queryDTO = new ConfirmOrderQueryDTO(dto.getMemberId(), date, trainCode);
            if (confirmOrderMapper.getConfirmOrder(queryDTO) != null) {
                throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_EXIST);
            }
        }

        Set<Integer> seatTypeSet = dto.getTicketList().parallelStream()
                .map(ConfirmOrderTicketDTO.PassengerTicket::getSeatTypeCode).collect(Collectors.toSet());

        //令牌大闸,余票查询
        this.validateTrainTicketToken(date, trainCode, seatTypeSet);

        //购票订单信息存入数据库
        ConfirmOrderTicketBO ticketBO = BeanUtil.copyProperties(dto, ConfirmOrderTicketBO.class);
        ticketBO.setDailyTrainTicket(dto.getId());
        ticketBO.initStatus();
        ticketBO.setTickets(dto.getTicketList());
        Long id = confirmOrderMapper.saveConfirmOrder(ticketBO);
        if (id == null) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_ORDER_SAVE_FAIL);
        }

        //发起确认订单主流程
        this.confirmOrderProducer.sendConfirmOrder(id, date, trainCode);

        return String.valueOf(id);
    }

    /**
     * 确认订单流程(确认以后还需要发起订单支付流程,暂时不涉及)
     * @param dto
     */
    @Override
    public void confirmProcess(ConfirmOrderEventDTO dto) {
        LocalDate date = dto.getDate();
        String trainCode = dto.getTrainCode();

        /**
         * 分布式锁+快速失败解决超卖, 实现抢票
         * key
         * 业务处理时间>key过期时间问题: 分布式锁持有时间设置的是5秒, 业务处理时间>5秒, 会提前自动释放锁导致锁失效, 依然会出现超卖
         */
//        String trainDate = LocalDateTimeUtils.format(date, LocalDateTimeUtils.DATE_FORMAT);
//        Long threadId = Thread.currentThread().getId();
//        Boolean isLock = redisSupport.setLock(KeyConstants.CONFIRM_ORDER_LOCK_KEY,
//                new Object[]{trainDate, trainCode},
//                threadId,
//                CommonConstants.CONFIRM_ORDER_LOCK_KEY_EXPIRE_TIME, TimeUnit.SECONDS);
//        if (!isLock) {
//            throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_LOCK);
//        }

        /**
         * 看门狗+分布式锁+超时等待+快速失败解决超卖, 实现抢票
         * 看门狗是一个守护线程在执行, 跟随用户线程开启和结束, 它会在最开始设置持有锁的超时时间为30秒, 当倒计时到一定时间的时候, 再次刷新为30秒,
         * 不断的倒计时和重置时间, 解决了业务处理时间>key过期时间问题
         */
        String trainDate = LocalDateTimeUtils.format(date, LocalDateTimeUtils.DATE_FORMAT);
        RLock lock = redissonSupport.getLock(KeyConstants.CONFIRM_ORDER_LOCK_KEY, trainDate, trainCode);

        try {
            if (!lock.tryLock(0L, TimeUnit.SECONDS)) {
//                throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_LOCK); //等待15秒后直接快速失败
                return; //没抢到锁就直接结束线程, 不等待
            }

            //循环排队出票
            while (true) {
                long startTime = System.currentTimeMillis();
                log.info("开始执行确认订单主流程,当前时间:{}", LocalDateTimeUtils.format(LocalDateTime.now(), LocalDateTimeUtils.DATE_TIME_FORMAT));

                //分页查找初始化的订单
                ConfirmOrderPageDTO pageDTO = new ConfirmOrderPageDTO(1L, 5L);
                pageDTO.setDate(date);
                pageDTO.setTrainCode(trainCode);
                pageDTO.setStatus(ConfirmOrderStatusEnums.INIT.getCode());
                PageResp<ConfirmOrderPageHandleOrderDTO> pageResp = confirmOrderMapper.pageListByTrainDateStaus(pageDTO);
                List<ConfirmOrderPageHandleOrderDTO> handleOrderDTOList = pageResp.getData();
                if (CollectionUtils.isEmpty(handleOrderDTOList)) {
                    break;
                }

                for (ConfirmOrderPageHandleOrderDTO handleOrderDTO : handleOrderDTOList) {
                    List<ConfirmOrderTicketDTO.PassengerTicket> passengerTickets
                            = JsonConvertUtils.convert(handleOrderDTO.getTickets(), ConfirmOrderTicketDTO.PassengerTicket.class);
                    handleOrderDTO.setTicketList(passengerTickets); //序列化获取乘客了列表
                    handleOrderDTO.setTrainCode(dto.getTrainCode());
                    handleOrderDTO.setDate(dto.getDate());

                    //处理订单
                    try {
                        this.handleOrder(date, trainCode, handleOrderDTO);
                    } catch (GlobalException e) {
                        log.error("订单确认主流程异常,订单id:{},异常信息:{}", handleOrderDTO.getId(), e.getMessage());
                        //根据失败条件更新订单状态, 继续执行下一笔订单
                        ConfirmOrderEditStatusDTO statusDTO = new ConfirmOrderEditStatusDTO();
                        statusDTO.setId(handleOrderDTO.getId());
                        boolean isEdit = true;
                        if (BizExceptionEnums.EXP_TRAIN_ORDER_TICKET_NOT_ENOUGH.getCode().equals(e.getCode())) {
                            //无票
                            statusDTO.setStatus(ConfirmOrderStatusEnums.EMPTY.getCode());
                            isEdit = confirmOrderMapper.editOrderStatus(statusDTO);
                        } else if (SysExceptionEnums.EXP_SERVER.getCode().equals(e.getCode()) ||
                            BizExceptionEnums.EXP_TRAIN_SEAT_NOT_EXISTS.getCode().equals(e.getCode()) ||
                                BizExceptionEnums.EXP_EDIT_CONFIRM_ORDER_STATUS.getCode().equals(e.getCode())) {
                            //系统异常
                            statusDTO.setStatus(ConfirmOrderStatusEnums.FAILURE.getCode());
                            isEdit = confirmOrderMapper.editOrderStatus(statusDTO);
                        } else {
                            //其他异常
                            statusDTO.setStatus(ConfirmOrderStatusEnums.FAILURE.getCode());
                            isEdit = confirmOrderMapper.editOrderStatus(statusDTO);
                        }
                        if (!isEdit) {
                            log.error("更新订单状态失败,订单id:{},异常信息:{}", handleOrderDTO.getId(), e.getMessage());
                            throw new GlobalException(BizExceptionEnums.EXP_EDIT_CONFIRM_ORDER_STATUS); //抛到外层消费者重试
                        }
                    }
                }

                log.info("确认订单主流程执行完毕,当前时间:{}, 耗时:{}秒",
                        LocalDateTimeUtils.format(LocalDateTime.now(), LocalDateTimeUtils.DATE_TIME_FORMAT),
                        (System.currentTimeMillis() - startTime) / 1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
//            Object id = redisSupport.getLock(KeyConstants.CONFIRM_ORDER_LOCK_KEY, trainDate, trainCode);
//            log.info("释放锁,当前线程:{},锁的id:{}", threadId, id);
//            //只有当前线程id等于锁的id才释放锁, 锁的id就是加锁线程的线程id
//            if (threadId.equals(SerializableUtils.parseObject(id, Long.class))) {
//                redisSupport.delete(KeyConstants.CONFIRM_ORDER_LOCK_KEY, trainDate, trainCode);
//            }
            redissonSupport.unlock(lock);
        }


    }

    /**
     * 处理订单
     * @param date
     * @param trainCode
     * @param dto
     */
    private void handleOrder(LocalDate date, String trainCode, ConfirmOrderPageHandleOrderDTO dto) {
        //更新订单状态, 避免面重复出票
        this.updateConfirmOrderStatus(dto.getId(), ConfirmOrderStatusEnums.PENDING.getCode());

        //查询余票信息
        DailyTrainTicketQueryOneResp dailyTrainTicket = this.queryDailyTrainTicket(dto, date, trainCode);

        //设置订单发车和到站时间
        dto.setStartTime(dailyTrainTicket.getStartTime());
        dto.setEndTime(dailyTrainTicket.getEndTime());

        //预扣减余票,判断余票是否足够
        this.preReduce(dto, dailyTrainTicket);

        //选择座位
        List<DailyTrainSeatQueryListResp> choiceSeatList = this.chooseSeat(date, trainCode, dto.getTicketList(), dailyTrainTicket);

        //判断是否选座成功, 如果选座失败, 更新订单状态为失败, 抛异常
        if (CollectionUtil.isEmpty(choiceSeatList)) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_ORDER_TICKET_NOT_ENOUGH);
        }

        //更新库存、座位、订单状态、售票
        ConfirmOrderService proxy = (ConfirmOrderService) AopContext.currentProxy();
        proxy.confirmAfterProcess(dto, choiceSeatList, dailyTrainTicket.getStartIndex(), dailyTrainTicket.getEndIndex());

    }

    /**
     * 购买成功后, 新增订单、更新车次座位售卖情况、扣减站票库存、新增乘客购票记录
     * @param dto
     * @param choiceSeatList
     * @param startIndex
     * @param endIndex
     */
    @Override
    @GlobalTransactional(timeoutMills = 12000)
    public void confirmAfterProcess(ConfirmOrderPageHandleOrderDTO dto, List<DailyTrainSeatQueryListResp> choiceSeatList,
                                    Integer startIndex, Integer endIndex) {
        LocalDate date = dto.getDate();
        String trainCode = dto.getTrainCode();

        //乘客座位更新到车次
        List<DailyTrainSeatEditChoiceSeatDTO> choiceSeatDTO
                = BeanUtil.copyToList(choiceSeatList, DailyTrainSeatEditChoiceSeatDTO.class);
        CommonResp dailyTrainSeatResp = dailyTrainSeatApi.batchEditDailyTrainSeat(choiceSeatDTO);
        if (!OpenFeignUtils.isSuccess(dailyTrainSeatResp)) {
            throw new GlobalException(OpenFeignUtils.getMessage(dailyTrainSeatResp));
        }

        //扣减车站车票库存
        List<DailyTrainTicketReduceTicketDTO> dtoList = new ArrayList<>();
        DailyTrainTicketReduceTicketDTO reduceTicketDTO;
        for (DailyTrainSeatQueryListResp seat : choiceSeatList) {
            reduceTicketDTO = new DailyTrainTicketReduceTicketDTO();
            reduceTicketDTO.setDate(date);
            reduceTicketDTO.setTrainCode(trainCode);
            reduceTicketDTO.setStartIndex(startIndex);
            reduceTicketDTO.setEndIndex(endIndex);
            reduceTicketDTO.setSell(seat.getSell());
            reduceTicketDTO.setSeatType(seat.getSeatType());
            dtoList.add(reduceTicketDTO);
        }
        CommonResp dailyTrainTicketResp = dailyTrainTicketApi.batchReduceDailyTrainTicket(dtoList);
        if (!OpenFeignUtils.isSuccess(dailyTrainTicketResp)) {
            throw new GlobalException(OpenFeignUtils.getMessage(dailyTrainSeatResp));
        }

        //添加售票信息
        List<ConfirmOrderTicketDTO.PassengerTicket> ticketList = dto.getTicketList();
        List<TicketDTO> ticketDTOList = new ArrayList<>();
        TicketDTO ticketDTO;
        for (ConfirmOrderTicketDTO.PassengerTicket passengerTicket : ticketList) {
            for (DailyTrainSeatQueryListResp dailyTrainSeat : choiceSeatList) {
                if (dailyTrainSeat.getPassengerId().equals(passengerTicket.getPassengerId())) {
                    ticketDTO = new TicketDTO();
                    ticketDTO.setMemberId(dto.getMemberId());
                    ticketDTO.setPassengerId(passengerTicket.getPassengerId());
                    ticketDTO.setPassengerName(passengerTicket.getPassengerName());
                    ticketDTO.setTrainDate(dto.getDate());
                    ticketDTO.setTrainCode(dto.getTrainCode());
                    ticketDTO.setCarriageIndex(dailyTrainSeat.getCarriageIndex());
                    ticketDTO.setSeatRow(dailyTrainSeat.getRow());
                    ticketDTO.setSeatCol(dailyTrainSeat.getCol());
                    ticketDTO.setStartStation(dto.getStart());
                    ticketDTO.setStartTime(dto.getStartTime());
                    ticketDTO.setEndStation(dto.getEnd());
                    ticketDTO.setEndTime(dto.getEndTime());
                    ticketDTO.setSeatType(String.valueOf(passengerTicket.getSeatTypeCode()));
                    ticketDTOList.add(ticketDTO);
                }
            }
        }
        CommonResp batchSaveTicketResp = ticketApi.batchSaveTicket(ticketDTOList);
        if (!OpenFeignUtils.isSuccess(batchSaveTicketResp)) {
            throw new GlobalException(OpenFeignUtils.getMessage(batchSaveTicketResp));
        }

        //更新订单为成功(实际开发中需要等待支付成功以后异步更新订单状态)
        boolean isUpdated = confirmOrderMapper.updateConfirmOrder(dto.getId(), ConfirmOrderStatusEnums.SUCCESS.getCode());
        if (!isUpdated) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_ORDER_UPDATE_FAIL);
        }
    }

    /**
     * 校验验证码
     * @param dto
     */
    @Override
    public void validateCaptcha(ConfirmOrderTicketDTO dto) {
        String captchaToken = dto.getCaptchaToken();
        Object object = redisSupport.getValue(KeyConstants.CONFIRM_ORDER_CAPTCHA_CODE_KEY, captchaToken);
        if (object == null) {
            throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_CAPTCHA_CODE_EXPIRED);
        }
        String captchaCode = SerializableUtils.parseObject(object, String.class);
        if (!captchaCode.equalsIgnoreCase(dto.getCaptcha())) {
            throw new GlobalException(BizExceptionEnums.EXP_ERROR_CAPTCHA);
        }
        redisSupport.delete(KeyConstants.CONFIRM_ORDER_CAPTCHA_CODE_KEY, captchaToken);
    }

    /**
     * 添加令牌大闸, 防止机器人刷票, 得到令牌锁的用户才能购票
     * @param date
     * @param trainCode
     * @param memberId
     */
    @Override
    public void preventTicketScalping(LocalDate date, String trainCode, Long memberId) {
        String trainDate = LocalDateTimeUtils.format(date, LocalDateTimeUtils.DATE_FORMAT);
        RLock lock = redissonSupport.getLock(KeyConstants.CONFIRM_ORDER_TICKET_SCALPING_LOCK_KEY, trainDate, trainCode, memberId);
        try {
            //持有锁5秒, 5秒后才能重新拥有锁
            if (!lock.tryLock(0L, 5L, TimeUnit.SECONDS)) {
                throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_TICKET_SCALPING);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            redissonSupport.unlock(lock);
        }
    }

    /**
     * 令牌大闸+缓存判断余票
     * 在这里不需要进入购票主流程判断库存, 效率比判断库存高
     * @param date
     * @param trainCode
     * @param seatTypeSet
     */
    @Override
    public void validateTrainTicketToken(LocalDate date, String trainCode, Set<Integer> seatTypeSet) {
        for (Integer seatTypeCode : seatTypeSet) {
            String seatType = String.valueOf(seatTypeCode);
            Object token = redisSupport.getValue(KeyConstants.CONFIRM_ORDER_TOKEN_KEY, date, trainCode, seatType);
            //如果缓存为空就去数据库查, 然后重新设置到缓存
            if (token == null) {
                RLock decrementTokenLock = redissonSupport.getLock(KeyConstants.CONFIRM_ORDER_DECREMENT_TOKEN_LOCK_KEY, date, trainCode, seatType);
                // 线程等待超时 10 秒就快速失败
                try {
                    if (!decrementTokenLock.tryLock(10, TimeUnit.SECONDS)) {
                        throw new GlobalException(BizExceptionEnums.EXP_CONFIRM_ORDER_WAIT_TIMEOUT);
                    }
                    token = redisSupport.getValue(KeyConstants.CONFIRM_ORDER_TOKEN_KEY, date, trainCode, seatType);
                    if (token == null) {
                        DailyTrainTokenQueryLeftDTO leftDTO = new DailyTrainTokenQueryLeftDTO(trainCode, date, seatType);
                        CommonResp getTokenResp = dailyTrainSkTokenApi.getSkToken(leftDTO);
                        if (!OpenFeignUtils.isSuccess(getTokenResp)) {
                            throw new GlobalException(OpenFeignUtils.getMessage(getTokenResp));
                        }
                        DailyTrainTokenQueryLeftResp leftResp = OpenFeignUtils.getResult(getTokenResp, DailyTrainTokenQueryLeftResp.class);
                        Long leftToken = leftResp.getLeftToken();
                        if (leftToken == null) {
                            throw new GlobalException(BizExceptionEnums.EXP_DAILY_TRAIN_SK_TOKEN_ZERO, seatType);
                        }
                        redisSupport.addValue(leftToken, 60L, TimeUnit.SECONDS, KeyConstants.CONFIRM_ORDER_TOKEN_KEY, date, trainCode, seatType);
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    redissonSupport.unlock(decrementTokenLock);
                }
            }
            //缓存扣减余票
            Long leftToken = redisSupport.decrement(KeyConstants.CONFIRM_ORDER_TOKEN_KEY, 1, date, trainCode, seatType);
            if (leftToken < 0) {
                String message = SeatTypeEnums.getByCode(seatType).getMessage();
                throw new GlobalException(BizExceptionEnums.EXP_DAILY_TRAIN_SK_TOKEN_ZERO, message);
            }
            //重新刷新缓存
            redisSupport.addValue(leftToken, 60L, TimeUnit.SECONDS, KeyConstants.CONFIRM_ORDER_TOKEN_KEY, date, trainCode, seatType);
            if (leftToken % 5 == 0) {
                //更新数据库
                DailyTrainSkTokenValidDTO validDTO = new DailyTrainSkTokenValidDTO(trainCode, date, seatType, 5L);
                CommonResp dailyTrainSkTokenResp = dailyTrainSkTokenApi.decrementSkToken(validDTO);
                if (!OpenFeignUtils.isSuccess(dailyTrainSkTokenResp)) {
                    throw new GlobalException(OpenFeignUtils.getMessage(dailyTrainSkTokenResp));
                }
            }
        }
    }


    /**
     * 查询余票信息
     * @param dto
     * @param date
     * @param trainCode
     * @return
     */
    @Override
    public DailyTrainTicketQueryOneResp queryDailyTrainTicket(ConfirmOrderPageHandleOrderDTO dto, LocalDate date, String trainCode) {
        CommonResp dailyTrainTicketResp = dailyTrainTicketApi.getDailyTrainTicket(
                DailyTrainTicketQueryOneDTO.builder()
                        .date(date)
                        .trainCode(trainCode)
                        .start(dto.getStart())
                        .end(dto.getEnd()).build()
        );
        if (!OpenFeignUtils.isSuccess(dailyTrainTicketResp)) {
            throw new GlobalException(OpenFeignUtils.getMessage(dailyTrainTicketResp));
        }
        return OpenFeignUtils.getResult(dailyTrainTicketResp, DailyTrainTicketQueryOneResp.class);
    }

    /**
     * 预扣减余票
     * @param dto
     * @param dailyTrainTicket
     */
    @Override
    public void preReduce(ConfirmOrderPageHandleOrderDTO dto, DailyTrainTicketQueryOneResp dailyTrainTicket) {
        Map<SeatTypeEnums, Supplier<Integer>> seatReduce = Map.of(
                SeatTypeEnums.YDZ, () -> dailyTrainTicket.getYdz() - 1,
                SeatTypeEnums.EDZ, () -> dailyTrainTicket.getEdz() - 1,
                SeatTypeEnums.RW, () -> dailyTrainTicket.getRw() - 1,
                SeatTypeEnums.YW, () -> dailyTrainTicket.getYw() - 1
        );

        Map<SeatTypeEnums, BiConsumer<DailyTrainTicketQueryOneResp, Integer> > seatUpdate = Map.of(
                SeatTypeEnums.YDZ, DailyTrainTicketQueryOneResp::setYdz,
                SeatTypeEnums.EDZ, DailyTrainTicketQueryOneResp::setEdz,
                SeatTypeEnums.RW, DailyTrainTicketQueryOneResp::setRw,
                SeatTypeEnums.YW, DailyTrainTicketQueryOneResp::setYw
        );

        for (ConfirmOrderTicketDTO.PassengerTicket passengerTicket : dto.getTicketList()) {
            SeatTypeEnums seatTypeEnums = SeatTypeEnums.getByCode(String.valueOf(passengerTicket.getSeatTypeCode()));
            Supplier<Integer> getLeftCount = seatReduce.get(seatTypeEnums);
            BiConsumer<DailyTrainTicketQueryOneResp, Integer> setSeatCount = seatUpdate.get(seatTypeEnums);
            if (getLeftCount != null && setSeatCount != null) {
                int countLeft = getLeftCount.get();
                if (getLeftCount.get() < 0) {
                    throw new GlobalException(BizExceptionEnums.EXP_TRAIN_ORDER_TICKET_NOT_ENOUGH);
                } else {
                    setSeatCount.accept(dailyTrainTicket, countLeft);
                }
            }
        }
    }

    /**
     * 选择座位
     * @param date
     * @param trainCode
     * @param ticketList
     * @param dailyTrainTicket
     * @return
     */
    @Override
    public List<DailyTrainSeatQueryListResp> chooseSeat(LocalDate date, String trainCode,
                                                        List<ConfirmOrderTicketDTO.PassengerTicket> ticketList,
                                                        DailyTrainTicketQueryOneResp dailyTrainTicket) {

        //选座集合
        List<DailyTrainSeatQueryListResp> choiceSeatList = new ArrayList<>();

        //如果有选择座位就计算偏移值
        if (ticketList.stream().noneMatch(ticket -> StrUtil.isBlank(ticket.getSeat()))) {

            //计算选座偏移值
            List<Integer> relativeOffsetList = this.determineOffset(ticketList);

            //获取座位类型
            Optional<ConfirmOrderTicketDTO.PassengerTicket> optional = ticketList.stream().findFirst();
            Integer seatTypeCode = optional.map(ConfirmOrderTicketDTO.PassengerTicket::getSeatTypeCode)
                    .orElse(null);
            if (seatTypeCode == null) {
                throw new GlobalException(SysExceptionEnums.EXP_SERVER);
            }

            //获取起始座位列
            String column = optional.map(passengerTicket -> passengerTicket.getSeat().split("")[0])
                    .orElse(null);
            if (StrUtil.isEmpty(column)) {
                throw new GlobalException(SysExceptionEnums.EXP_SERVER);
            }

            //获取乘客id
            List<Long> passengerIdList = ticketList.stream()
                    .map(ConfirmOrderTicketDTO.PassengerTicket::getPassengerId).toList();

            //选座位
            this.processChooseSeat(date
                    , passengerIdList
                    , null
                    , trainCode
                    , seatTypeCode
                    , column
                    , relativeOffsetList
                    , dailyTrainTicket.getStartIndex()
                    , dailyTrainTicket.getEndIndex()
                    , choiceSeatList);
        } else {
            //没选择座位就直接默认选座
            for (ConfirmOrderTicketDTO.PassengerTicket passengerTicket : ticketList) {
                //选座位
                this.processChooseSeat(date
                        , null
                        , passengerTicket.getPassengerId()
                        , trainCode
                        , passengerTicket.getSeatTypeCode()
                        , null
                        , null
                        , dailyTrainTicket.getStartIndex()
                        , dailyTrainTicket.getEndIndex(),
                        choiceSeatList);
            }
        }

        return choiceSeatList;
    }

    /**
     * 计算选座偏移值
     * @param ticketList
     */
    private List<Integer> determineOffset(List<ConfirmOrderTicketDTO.PassengerTicket> ticketList) {
        //根据座位类型构建座位列表
        List<String> seatList = new ArrayList<>();
        SeatColEnums[] seatColEnums = SeatColEnums.getByType(String.valueOf(ticketList.get(0).getSeatTypeCode()));
        for (int i = 1; i <= 2; i++) {
            for (SeatColEnums seatColEnum : seatColEnums) {
                seatList.add(seatColEnum.getCode() + i);
            }
        }

        //计算绝对偏移量
        List<Integer> absulateOffsetList = new ArrayList<>();
        for (ConfirmOrderTicketDTO.PassengerTicket passengerTicket : ticketList) {
            String seat = passengerTicket.getSeat();
            absulateOffsetList.add(seatList.indexOf(seat));
        }

        //计算相对偏移量
        List<Integer> relativeOffsetList = new ArrayList<>();
        for (Integer index : absulateOffsetList) {
            relativeOffsetList.add(index - absulateOffsetList.get(0));
        }
        return relativeOffsetList;
    }

    /**
     * 选座位核心处理逻辑
     *
     * @param date               车次日期
     * @param passengerIdList
     * @param passengerId
     * @param trainCode          车次
     * @param seatTypeCode       选座类型
     * @param relativeOffsetList 座位相对偏移量
     * @param startIndex         发车站序号
     * @param endIndex           终点站序号
     * @param choiceSeatList     最终选择的座位列表
     */
    private void processChooseSeat(LocalDate date, List<Long> passengerIdList, Long passengerId, String trainCode, Integer seatTypeCode, String column,
                                   List<Integer> relativeOffsetList, Integer startIndex, Integer endIndex,
                                   List<DailyTrainSeatQueryListResp> choiceSeatList) {
        //查询同一个座位类型的所有车厢
        CommonResp trainCarriageResp = dailyTrainCarriageApi.queryTrainCarriageList(
                        DailyTrainCarriageQueryListDTO.builder()
                                .date(date)
                                .trainCode(trainCode)
                                .seatTypeCode(seatTypeCode)
                                .build());
        List<DailyTrainCarriageQueryListResp> dailyTrainCarriageList
                = OpenFeignUtils.getResultList(trainCarriageResp, DailyTrainCarriageQueryListResp.class);
        if (CollectionUtil.isEmpty(dailyTrainCarriageList)) {
            String message = String.format("车次[%s]日期[%s]座位类型[%s]车厢不存在",
                    trainCode, date, SeatTypeEnums.getByCode(String.valueOf(seatTypeCode)).getMessage());
            throw new GlobalException(message);
        }

        //得到所有车厢的箱号,从小到大序列
        List<Integer> carriageIndexList = dailyTrainCarriageList.stream()
                .map(DailyTrainCarriageQueryListResp::getIndex)
                .toList();

        //查询所有车厢的座位
        CommonResp trainSeatResp = dailyTrainSeatApi.queryTrainSeatList(
                DailyTrainSeatQueryListDTO.builder()
                        .date(date)
                        .trainCode(trainCode)
                        .carriageIndexList(carriageIndexList)
                        .build()
        );
        List<DailyTrainSeatQueryListResp> dailyTrainSeatList
                = OpenFeignUtils.getResultList(trainSeatResp, DailyTrainSeatQueryListResp.class);
        if (CollectionUtil.isEmpty(dailyTrainSeatList)) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_SEAT_NOT_EXISTS);
        }

        //以厢序分组,获取每个车厢号对应的的座位
        Map<Integer, List<DailyTrainSeatQueryListResp>> seatMap = dailyTrainSeatList.stream()
                .collect(Collectors.groupingBy(DailyTrainSeatQueryListResp::getCarriageIndex));

        //按照车厢序号遍历每个车厢, 获取每个车厢的座位
        for (Integer carriageIndex : carriageIndexList) {
            //退出选座标志位
            boolean stopChooseSeat = false;

            //对车厢的座位排序
            List<DailyTrainSeatQueryListResp> seatList = CollectionUtil
                    .sort(seatMap.get(carriageIndex),
                    Comparator.comparingInt(DailyTrainSeatQueryListResp::getCarriageSeatIndex));

            log.info("厢序为: {}, 座位数是: {}", carriageIndex, seatList.size());

            //遍历车厢每一个座位, 判断该座位是否可以选座
            for (int i = 0; i < seatList.size(); i++) {

                DailyTrainSeatQueryListResp seat = seatList.get(i);

                //获取车厢座位号
                String col = seat.getCol();

                //判断column是否为空, 不为空的话,判断乘客所选的座位是否是column起始座位, 不是则跳过本次循环
                if (StrUtil.isNotEmpty(column) && !col.equals(column)) {
                    continue;
                } else if (StrUtil.isEmpty(column)) {
                    //当没有选择座位时, 判断该座位是否被选中过, 如果被选中过则看下一个座位
                    if (choiceSeatList.stream().anyMatch(choiceSeat -> choiceSeat.getId().equals(seat.getId()))) {
                        continue;
                    }
                }

                //对起始座位选座column选座, 如果失败则跳过,重新循环
                boolean isChose = this.isChoseSeat(seat, startIndex, endIndex);
                if (!isChose) {
                    log.info("车厢号[{}]座位序号[{}]选座失败, 座位为[{}]", seat.getCarriageIndex(), seat.getCarriageSeatIndex(), seat.getSell());
                    continue;
                } else {
                    if (CollectionUtil.isEmpty(passengerIdList)) {
                        seat.setPassengerId(passengerId);
                    } else {
                        seat.setPassengerId(passengerIdList.get(0)); //添加第一个乘客
                    }
                    choiceSeatList.add(seat);
                }

                //获取后续座位偏移量的起始下标
                int j = 1;

                //如果相对偏移量不为空, 就进行后续座位的选座
                if (CollectionUtil.isNotEmpty(relativeOffsetList)) {
                    //退出本次车厢循环标志位
                    boolean stop = false;

                    //偏移量从索引为1开始进行后续座位选座(起始座位在偏移量relativeOffsetList的下标是0)
                    for (; j < relativeOffsetList.size(); j++) {

                        //下一个选座的索引 = 起始座位索引(i) + 下一个选座的相对偏移量(relativeOffsetList.get(j))
                        int nextIndex = i + relativeOffsetList.get(j);

                        //如果下一个选座的索引大于座位总数, 则跳出本次和本次车厢, 进入下一个车厢重新开始
                        if (nextIndex > seatList.size()) {
                            stop = true;
                            break;
                        }

                        //获取下一个座位
                        DailyTrainSeatQueryListResp nextSeat = seatList.get(nextIndex);

                        //对这个索引的座位进行选座, 如果失败则跳出本次循环,在本车厢中重新开始
                        boolean isChoseNext = this.isChoseSeat(nextSeat, startIndex, endIndex);
                        if (!isChoseNext) {
                            log.info("车厢号[{}]座位序号[{}]选座失败, 座位为[{}]", seat.getCarriageIndex(), seat.getCarriageSeatIndex(), seat.getSell());
                            choiceSeatList.clear(); //清空本车厢选座列表
                            break;
                        } else {
                            if (CollectionUtil.isEmpty(passengerIdList)) {
                                nextSeat.setPassengerId(passengerId);
                            } else {
                                nextSeat.setPassengerId(passengerIdList.get(j)); //添加后续乘客id
                            }
                            choiceSeatList.add(nextSeat);
                        }
                    }

                    //根据stop标志位判断是否跳出本次车厢的循环
                    if (stop) {
                        choiceSeatList.clear(); //清空本车厢选座列表
                        break;
                    }
                }

                //第一个人选座成功后,没有后续选座就退出选座
                if (CollectionUtil.isEmpty(relativeOffsetList) || relativeOffsetList.size() == j) {
                    stopChooseSeat = true;
                    break;
                }
            }

            //退出选座
            if (stopChooseSeat) {
                break;
            }
        }
    }

    /**
     * 判断是否选座成功
     * @param seat 每趟车厢的座位
     * @param startIndex 发车站序号
     * @param endIndex 终点站序号
     * @return
     */
    private boolean isChoseSeat(DailyTrainSeatQueryListResp seat, Integer startIndex, Integer endIndex) {
        //得到已销售的选座
        String sellSeat = seat.getSell();

        //根据start和end截取选座的区间
        String seatRange = sellSeat.substring(startIndex, endIndex);

        //判断选座区间内的座位是否大于0, 如果大于0, 则选座失败
        if (Long.parseLong(seatRange) > 0) {
            return false;
        }

        //将截取的区间全部置为1
        seatRange = seatRange.replace('0', '1');

        //在截取的区间的左边补0
        seatRange = StrUtil.fillBefore(seatRange, '0', endIndex);

        //在截取区间的右边补0
        seatRange = StrUtil.fillAfter(seatRange, '0', sellSeat.length());

        //得到的新区间和原来选座区间进行或运算, 得到新的选座, 得到十进制
        long newSellSeat = NumberUtil.binaryToLong(sellSeat) | NumberUtil.binaryToLong(seatRange);

        //将十进制的新选座转为二进制位
        String binarySellSeat = NumberUtil.getBinaryStr(newSellSeat);

        //在新选座的二进制位左边补0
        binarySellSeat = StrUtil.fillBefore(binarySellSeat, '0', sellSeat.length());

        //更新的选座二进制位重新设置到座位中
        seat.setSell(binarySellSeat);

        log.info("车厢号[{}]座位序号[{}]选座成功, 更新座位为[{}]", seat.getCarriageIndex(), seat.getCarriageSeatIndex(), binarySellSeat);

        return true;
    }

    /**
     * 更新订单状态
     * @param id
     * @param status
     */
    @Override
    public void updateConfirmOrderStatus(Long id, String status) {
        ConfirmOrderEditStatusDTO statusDTO = new ConfirmOrderEditStatusDTO();
        statusDTO.setId(id);
        statusDTO.setStatus(status);
        boolean isEdit = confirmOrderMapper.editOrderStatus(statusDTO);
        if (!isEdit) {
            throw new GlobalException(BizExceptionEnums.EXP_EDIT_CONFIRM_ORDER_STATUS);
        }
    }

    @Override
    public String queryWaitingStatus(Long orderId) {
        //查询该订单
        ConfirmOrderPO confirmOrderPO = confirmOrderMapper.selectById(orderId);
        if (confirmOrderPO == null) {
            throw new GlobalException(BizExceptionEnums.EXP_QUERY_CONFIRM_ORDER_NOT_EXIST);
        }

        //获取status是confirmOrderPO.getStatus()的枚举对象, 如果是init状态则获取排队等待人数
        ConfirmOrderStatusEnums confirmOrderStatusEnum = EnumUtil.getBy(ConfirmOrderStatusEnums::getCode, confirmOrderPO.getStatus());
        return switch (confirmOrderStatusEnum) {
            case SUCCESS -> "-1";
            case CANCEL -> "-2";
            case FAILURE -> "-3";
            case EMPTY -> "-4";
            case INIT -> {
                long countNumber = confirmOrderMapper.countNumber(confirmOrderPO);
                yield String.valueOf(countNumber);
            }
            case PENDING -> "0";
        };
    }

    @Override
    public void cancelOrderConfirm(Long orderId) {
        ConfirmOrderEditStatusDTO statusDTO
                = new ConfirmOrderEditStatusDTO(orderId, ConfirmOrderStatusEnums.CANCEL.getCode());
        boolean isEdit = confirmOrderMapper.editInitOrderStatus(statusDTO);
        if (!isEdit) {
            throw new GlobalException(BizExceptionEnums.EXP_CANCEL_CONFIRM_ORDER_STATUS);
        }
    }
}




